package cn.seaboot.admin.security.token;

import cn.seaboot.admin.security.auth.SecurityAuthorityProvider;
import cn.seaboot.admin.security.jwt.AuthenticationJwt;
import cn.seaboot.admin.security.jwt.SecurityJwtProperties;
import io.jsonwebtoken.Claims;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

/**
 * token 校验拦截
 * <p>
 * 这里包含 token 的校验逻辑，最终的效果，与 LoginFilter 一致，上下文中，
 * 会保存有经过认证的用户信息，供后续的鉴权逻辑使用。
 *
 * @author Mr.css
 * @version 2021-12-23 15:14
 * @see SecurityContextHolder
 * @see cn.seaboot.admin.security.auth.AntAuthorityAccessDecision
 */
@Component
public class TokenFilter extends OncePerRequestFilter {
    private final Logger logger = LoggerFactory.getLogger(TokenFilter.class);

    /**
     * 令牌执行对象
     */
    @Resource
    private AuthenticationJwt authenticationJwt;

    @Resource
    private SecurityJwtProperties securityJwtProperties;

    @Resource
    private SecurityJwtProperties securityJwtConstant;

    @Resource
    private SecurityAuthorityProvider securityAuthorityProvider;

    /**
     * 与doFilter相同的契约，但保证在单个请求线程中每个请求只调用一次。详情请参见shouldNotFilterAsyncDispatch()。
     * 提供HttpServletRequest和HttpServletResponse参数，而不是默认的ServletRequest和ServletResponse参数。
     *
     * @param request     请求
     * @param response    响应
     * @param filterChain 过滤链
     * @throws ServletException -
     * @throws IOException      -
     */
    @Override
    protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
        if (securityJwtConstant.isDebug()) {
            this.doDebug(request, response, filterChain);
            return;
        }

        logger.debug("[security token filter] check servlet path: {}", request.getServletPath());

        // 允许头部或者参数中携带 token
        String jwt = request.getHeader(securityJwtConstant.getTokenName());
        if (jwt == null) {
            jwt = request.getParameter(securityJwtConstant.getTokenName());
        }

        if (jwt == null) {
            // 因为有匿名访问这一功能，如果 token 为空，仍然进入 url 匹配校验
            // 已知问题：如果前一个用户没安全退出，其它人可以使用缓存中的用户继续使用系统
            // TODO: 感觉这里存在漏洞：构建一个匿名用户的上下文可能会更好
            logger.debug("[security token filter] anonymous user!");
            filterChain.doFilter(request, response);
            return;
        }

        // 验签，任意异常都以未授权的方式抛出
        Claims claims;
        try {
            claims = authenticationJwt.decrypt(jwt);
        } catch (Exception e) {
            // token 过期
            throw new AccessDeniedException("token expired!");
        }

        // 如果上下文中已经存在 token，则直接使用上下文中的
        UserToken token = this.getContextUserToken(request);

        if (token == null) {
            // 理论上，上下文中应当缓存用户，如果不存在，可能是 session 正常超时；
            // 也有可能是受到攻击，令牌被异常使用，要求重新登录。
            throw new AccessDeniedException("lost user context!");
        } else {
            if (!claims.getSubject().equals(token.getPrincipal())) {
                // 上下文不一致，合理怀疑受到攻击，令牌被异常使用，要求重新登录。
                throw new AccessDeniedException("inconsistent user context!");
            }
        }

        // 继续走其他过滤链
        filterChain.doFilter(request, response);
    }

    /**
     * 使用 debug 模式进行开发
     *
     * 自动使用测试账号进行调试，此时系统的鉴权系统是正常运作的，只是会自动完成鉴权步骤而已。
     *
     * @param request     -
     * @param response    -
     * @param filterChain -
     * @throws IOException      -
     * @throws ServletException -
     */
    private void doDebug(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        logger.debug("[security token filter] debug ant path: {}", request.getServletPath());
        // 如果上下文中已经存在 token，则直接使用上下文中的
        UserToken token = this.getContextUserToken(request);
        if (token != null) {
            filterChain.doFilter(request, response);
            return;
        }

        // 默认使用指定的身份进行登录
        String username = securityJwtConstant.getUsername();

        // 如果 jwt 不为空，尝试解析，并且使用 jwt 指定的账号进行操作
        String jwt = request.getHeader(securityJwtConstant.getTokenName());
        if (jwt == null) {
            jwt = request.getParameter(securityJwtConstant.getTokenName());
        }
        if (jwt != null) {
            try {
                Claims claims = authenticationJwt.decrypt(jwt);
                username = claims.getSubject();
            } catch (Exception e) {
                e.printStackTrace();
                throw new AccessDeniedException("The security token expired!");
            }
        }

        // 使用账号，构建一个经过认证的身份令牌
        token = this.createUserToken(request, username);

        // 将令牌添加到会话中去
        if (securityJwtProperties.isUsingSession()) {
            request.getSession().setAttribute(securityJwtProperties.getTokenName(), token);
        }

        logger.debug("[security token filter] using debug user: {}", SecurityContextHolder.getContext().getAuthentication());
        filterChain.doFilter(request, response);
    }

    /**
     * 构建一个身份令牌
     *
     * @param request  -
     * @param username 账号
     * @return 令牌
     */
    private UserToken createUserToken(@NotNull HttpServletRequest request, String username) {
        List<GrantedAuthority> grantedAuthorities = securityAuthorityProvider.queryGrantedAuthority(username);

        UserToken token = new UserToken(username, null, grantedAuthorities);
        SecurityContextHolder.getContext().setAuthentication(token);
        token.setDetails(new WebAuthenticationDetails(request));
        return token;
    }

    /**
     * 获取上下文之间的身份令牌
     *
     * @return 身份令牌
     */
    @Nullable
    private UserToken getContextUserToken(@NotNull HttpServletRequest request) {
        UserToken token = (UserToken) SecurityContextHolder.getContext().getAuthentication();
        if (token != null) {
            // 如果上下文中已经存在 token，则直接使用上下文中的
            logger.debug("[security token filter] using context token: {}", token);
            return token;
        } else if (securityJwtProperties.isUsingSession()) {
            // 使用会话缓存中的账号信息
            token = (UserToken) request.getSession().getAttribute(securityJwtProperties.getTokenName());
            if (token != null) {
                logger.debug("[security token filter] using cache token : {}", token);
                return token;
            }
        }
        return null;
    }
}
